package org.codefilarete.tool;

import java.time.Clock;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

/**
 * Tool class around Date concept.
 * 
 * @author Guillaume Mary
 */
public class Dates {
	
	/** System clock, for clean code */
	private static final Clock SYSTEM_CLOCK = Clock.systemDefaultZone();
	
	/** The clock to be used to give referent dates and times, default is system one */
	private static Clock clock = Clock.systemDefaultZone();
	
	/**
	 * Executes some code at a given {@link LocalDateTime}. Made to test some code in past or future to ensures that some code doesn't depend on
	 * system current millis.
	 * Be aware that this method will ask for a lock on a static-shared instance, therefore will block until the lock is released.
	 * 
	 * Prefer {@link #doWithClock(Clock, Runnable)} for a more fine-grained method
	 * 
	 * @param runtime the instant to be used when running code
	 * @param runnable the code to be executed
	 * @see #doWithClock(Clock, Runnable) 
	 */
	public static void runAtTime(LocalDateTime runtime, Runnable runnable) {
		ZoneId zoneId = ZoneId.systemDefault();
		doWithClock(Clock.fixed(runtime.toInstant(zoneId.getRules().getOffset(runtime)), zoneId), runnable);
	}
	
	/**
	 * Executes some code at a given {@link Clock}. Made to test some code in past or future to ensures that some code doesn't depend on system clock.
	 * Be aware that this method will ask for a lock on a static-shared instance, hence will block until the lock is released.
	 * 
	 * Prefer {@link #runAtTime(LocalDateTime, Runnable)} for a simplier use case.
	 * 
	 * @param newClock the clock to be used when running code
	 * @param runnable the code to be executed
	 * @see #runAtTime(LocalDateTime, Runnable) 
	 */
	public static void doWithClock(Clock newClock, Runnable runnable) {
		synchronized (SYSTEM_CLOCK) {
			clock = newClock;
			try {
				runnable.run();
			} finally {
				clock = SYSTEM_CLOCK;
			}
		}
	}
	
	/**
	 * Gives "now instant".
	 * Should be used in preference to {@link LocalDateTime#now()} for code that must be tested in future or past because this
	 * method will take {@link Clock} set on {@link Dates} class which can be changed when using {@link #doWithClock(Clock, Runnable)}.
	 * 
	 * @return "now instant" according to clock defined in this class, which is the system one by default
	 */
	public static LocalDateTime now() {
		return LocalDateTime.now(clock);
	}
	
	/**
	 * Gives "today".
	 * Should be used in preference to {@link LocalDate#now()} for code that must be tested in future or past because this
	 * method will take {@link Clock} set on {@link Dates} class which can be changed when using {@link #doWithClock(Clock, Runnable)}.
	 *
	 * @return "now instant" according to clock defined in this class, which is the system one by default
	 */
	public static LocalDate today() {
		return LocalDate.now(clock);
	}
	
	/**
	 * Gives "now instant" as an old-fashion {@link Date}.
	 * Should be used in preference to {@link Date#Date() new Date()} for code that must be tested in future or past because this
	 * method will take {@link Clock} set on {@link Dates} class which can be changed when using {@link #doWithClock(Clock, Runnable)}.
	 *
	 * @return "now instant" according to clock defined in this class, which is the system one by default
	 */
	public static Date nowAsDate() {
		return Date.from(now().atZone(ZoneId.systemDefault()).toInstant());
	}
}
